가상 면접 사례로 배우는 생성형 AI 시스템 설계 — 2장 지메일 스마트 편지쓰기

출처: 가상 면접 사례로 배우는 생성형 AI 시스템 설계 (알리 아미니안, 알렉스 쉬) 2026 현실 업데이트: 책의 핵심 개념을 유지하면서, 실무에서 실제로 쓰는 방식으로 재구성했다.


2026년 기준 — 스마트 편지쓰기를 만드는 두 가지 길

책은 트랜스포머를 처음부터 설계·학습하는 시나리오를 가정한다. 그러나 2026년 현실에서 "스마트 편지쓰기"를 만드는 방식은 사실 두 갈래다.

[경로 A] API 활용 (99%의 프로덕션 스타트업)
  사용자 입력 → Claude/GPT-4o API (스트리밍) → 제안 표시
  구현: 2시간 / 비용: 토큰당 과금 / 장점: 즉시 배포

[경로 B] 모델 직접 파인튜닝 (대형 서비스, 구글 수준)
  자체 데이터 → 오픈소스 모델(Llama 3.2, Gemma 3) 파인튜닝 → 서빙
  구현: 수주 / 비용: GPU 인프라 / 장점: 커스터마이징, 비용 절감

이 장의 내용은 경로 B의 설계 원리다. 경로 A로 시작해서 트래픽이 쌓이면 경로 B로 전환하는 것이 2026년 실무 패턴이다.


전체 흐름도 (책 원본 → 2026 매핑)

[책의 개념]                   [2026 실제 도구]
─────────────────────────────────────────────────
 요구사항 구체화               그대로 (불변)
 ML 문제 정의 (디코더 전용)    GPT/Gemma/Llama 계열
 데이터 준비 (BPE 토큰화)      tiktoken, HF tokenizers
 사전 학습                     건너뜀 → 사전학습된 모델 다운로드
 미세 조정                     LoRA / QLoRA (PEFT)
 샘플링 (빔 검색)              HF generate() API
 평가 (Perplexity)             evaluate 라이브러리, LangSmith
 시스템 설계 (트리거/후처리)   FastAPI + vLLM 서빙

선수 지식 체크리스트

  • [ ] Python 기초 (리스트, 딕셔너리, 함수)
  • [ ] pip install transformers torch 실행 가능
  • [ ] API 키 발급 경험 (OpenAI, Anthropic 중 하나)
  • [ ] 벡터와 행렬의 개념 (곱셈 결과 차원을 계산할 수 있으면 충분)

핵심 키워드

용어 2026 실무 의미
디코더 전용 트랜스포머 GPT, Claude, Llama, Gemma의 기반 구조. "다음 토큰 예측" 기계
토큰화 (BPE) tiktoken.get_encoding("cl100k_base") 한 줄로 사용
텍스트 임베딩 모델 내부 lookup table. 직접 구현할 일 없음
위치 인코딩 RoPE(Llama), ALiBi 등으로 진화. 개념만 알면 됨
사전 학습 이미 된 것을 다운로드. Meta/Google/Anthropic이 수십억 달러 투자
미세 조정 LoRA로 1~2% 파라미터만 학습. A100 1장으로 가능
빔 검색 model.generate(num_beams=4) 한 줄
Perplexity evaluate.load("perplexity") 라이브러리 제공
vLLM 프로덕션 LLM 서빙 엔진. PagedAttention으로 처리량 24배

2.1 요구사항 구체화

핵심 (변경 없음)

면접에서 가장 먼저 해야 할 것은 범위 확정이다. 이 단계는 2026년에도 그대로다.

요구사항 2026 코멘트
응답 시간 ≤100ms API 기반이면 200~500ms가 현실. 직접 서빙하면 vLLM으로 달성 가능
동시 사용자 18억 명 API 기반이면 rate limit 협상 문제
편향 방지 필수 Llama Guard 2/3 같은 안전 모델을 파이프라인에 추가
개인화 우선 미포함 2026에는 RAG + 사용자 히스토리로 쉽게 추가 가능

2.2 ML 문제 정의

한 줄 요약

"다음 토큰 예측" — 이것이 LLM의 전부다. 2026년에도 변하지 않은 핵심.

왜 트랜스포머인가 (아직도 유효)

RNN (LSTM, GRU)         트랜스포머
─────────────           ──────────────
토큰을 하나씩 처리      모든 토큰 동시 처리 (병렬)
기울기 소실 문제        셀프 어텐션으로 장거리 문맥 유지
2024년 이후 거의 안 씀  GPT/Claude/Gemini 모두 이것

2026년에 더 중요해진 것: 트랜스포머 3종 선택

# 작업에 따라 모델 선택이 달라진다

# 분류/이해 작업 → 인코더 전용 (BERT 계열)
from transformers import BertForSequenceClassification
model = BertForSequenceClassification.from_pretrained("bert-base-uncased")

# 텍스트 생성 (스마트 편지쓰기) → 디코더 전용 ✅
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.2-3B")

# 번역/요약 → 인코더-디코더 (T5 계열)
from transformers import T5ForConditionalGeneration
model = T5ForConditionalGeneration.from_pretrained("google/flan-t5-base")

2.3 데이터 준비

한 줄 요약

2026년 실무: 정제·토큰화 로직을 직접 짜되, 토큰화 라이브러리는 가져다 쓴다.

2.3.1 텍스트 정제 — 실제 코드

import re
from presidio_analyzer import AnalyzerEngine  # pip install presidio-analyzer

# 기밀 정보 마스킹 (Presidio: Microsoft 오픈소스)
analyzer = AnalyzerEngine()

def clean_email(text: str) -> str:
    """이메일 텍스트 정제: 기밀 정보 마스킹 + 불필요 기호 제거"""

    # 1) Presidio로 PII 탐지 후 마스킹
    results = analyzer.analyze(text=text, language="en")
    for result in sorted(results, key=lambda x: x.start, reverse=True):
        text = text[:result.start] + "[REDACTED]" + text[result.end:]

    # 2) 불필요한 기호 제거 (이모티콘, ©, ™ 등)
    text = re.sub(r'[^\x00-\x7F]+', ' ', text)  # 비 ASCII
    text = re.sub(r'[©™®]', '', text)

    # 3) 연속 공백 정규화
    text = re.sub(r'\s+', ' ', text).strip()
    return text

# 사용 예시
raw = "Hi john@gmail.com, call me at 010-1234-5678 ©2024"
print(clean_email(raw))
# → "Hi [REDACTED], call me at [REDACTED] 2024"

2.3.2 텍스트 토큰화 — 3종 비교 후 BPE 채택

import tiktoken  # pip install tiktoken

# GPT-4o가 쓰는 BPE 토크나이저 (cl100k_base)
enc = tiktoken.get_encoding("cl100k_base")

text = "Let's go to NYC"
tokens = enc.encode(text)
print(tokens)      # [10267, 596, 733, 311, 38103]
print(enc.decode(tokens))  # "Let's go to NYC"

# 어휘집 크기 확인
print(enc.n_vocab)  # 100,277개 (하위 단어 단위)

왜 BPE인가 — 직관적 비교:

text = "unhappily"

# 문자 단위: 어휘집 작지만 의미 없음
char = list("unhappily")  # ['u','n','h','a','p','p','i','l','y']

# 단어 단위: 어휘집 너무 큼, 미등록 단어 처리 불가
# word = ["unhappily"]  # 300,000+ 어휘집 필요

# BPE (하위 단어): 균형 ✅
bpe = enc.encode("unhappily")
decoded = [enc.decode([t]) for t in bpe]
print(decoded)  # ['un', 'happ', 'ily'] 처럼 의미 단위로 분해
방식 어휘집 크기 미등록 단어 실무 사용
문자 단위 100~1,000 없음 거의 안 씀
단어 단위 300,000+ 처리 어려움 거의 안 씀
BPE (하위 단어) 50,000~150,000 분해 처리 GPT-4, Claude, Gemma

2.3.3 HuggingFace Datasets로 데이터 파이프라인

from datasets import load_dataset
from transformers import AutoTokenizer

# 이메일 데이터셋 로드 (Enron 공개 데이터)
dataset = load_dataset("aeslc", split="train")  # 이메일 요약 데이터

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.2-3B")
tokenizer.pad_token = tokenizer.eos_token

def tokenize_fn(examples):
    """이메일 → 다음 토큰 예측 형식으로 변환"""
    # 프롬프트 형식: [SUBJECT] {제목} [BODY] {본문}
    texts = [
        f"[SUBJECT] {s} [BODY] {b}"
        for s, b in zip(examples["subject"], examples["body"])
    ]
    return tokenizer(
        texts,
        truncation=True,
        max_length=512,
        padding="max_length"
    )

tokenized = dataset.map(tokenize_fn, batched=True)

2.4 모델 개발

2.4.1 트랜스포머 구조 — 2026년 시각

책의 4요소(임베딩→위치인코딩→트랜스포머블록→예측헤드)는 여전히 유효하다. 다만 직접 구현할 일은 없고, 어떤 레이어가 무슨 역할인지 이해하는 것이 중요하다.

from transformers import AutoModelForCausalLM
import torch

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.2-3B",
    torch_dtype=torch.bfloat16,  # 메모리 절약
    device_map="auto"            # GPU/CPU 자동 배분
)

# 내부 구조 확인
print(model)
# LlamaForCausalLM(
#   (model): LlamaModel(
#     (embed_tokens): Embedding(128256, 3072)   ← 텍스트 임베딩
#     (layers): ModuleList(
#       (0-27): 28 x LlamaDecoderLayer(
#         (self_attn): LlamaSdpaAttention(...)   ← 셀프 어텐션
#         (mlp): LlamaMLP(...)                   ← 순방향 신경망
#       )
#     )
#     (norm): LlamaRMSNorm(...)
#   )
#   (lm_head): Linear(3072, 128256)             ← 예측 헤드
# )

위치 인코딩: 2026년 현실

책에서 배운 것      →  실제 최신 모델
─────────────────────────────────────
사인-코사인 인코딩  →  RoPE (Llama, Gemma, Qwen)
학습 위치 인코딩    →  ALiBi (일부 모델)

RoPE가 2026년 사실상 표준. 상대 위치를 어텐션 계산 안에서 처리해서
더 긴 컨텍스트(128K+)로 자연스럽게 확장 가능.

2.4.2 사전 학습 — 2026년에는 "다운로드"

# 2026년 현실: 처음부터 사전 학습은 구글/Meta/Anthropic만 한다.
# 우리는 HuggingFace에서 다운로드.

from huggingface_hub import snapshot_download

# 실제 사용 예시들
models = {
    "초소형 (모바일/엣지)":  "google/gemma-3-1b-it",      # 1B
    "소형 (서버 1대)":       "meta-llama/Llama-3.2-3B",   # 3B
    "중형 (서버 2-4대)":     "meta-llama/Llama-3.1-8B",   # 8B
    "대형 (클러스터)":       "meta-llama/Llama-3.1-70B",  # 70B
}

# 스마트 편지쓰기: 3B~8B 정도가 품질/속도 균형 최적
model_id = "meta-llama/Llama-3.2-3B-Instruct"
snapshot_download(model_id, local_dir="./models/llama-3.2-3b")

2.4.3 미세 조정 — LoRA로 효율적으로

2026년 파인튜닝의 핵심은 LoRA (Low-Rank Adaptation)다. 전체 파라미터 대신 1~2%의 어댑터만 학습해 A100 1장으로 8B 모델 파인튜닝이 가능하다.

# pip install peft trl transformers accelerate bitsandbytes

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, TaskType
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset
import torch

# 1) 4비트 양자화로 모델 로드 (메모리 절약)
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.2-3B",
    quantization_config=bnb_config,
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.2-3B")
tokenizer.pad_token = tokenizer.eos_token

# 2) LoRA 어댑터 설정 (전체 파라미터의 ~1%만 학습)
lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,                              # 랭크: 작을수록 효율적, 클수록 표현력
    lora_alpha=32,                     # 스케일링 팩터
    target_modules=["q_proj", "v_proj"],  # 어텐션 레이어에 적용
    lora_dropout=0.05,
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 4,194,304 || all params: 3,216,215,040 || trainable%: 0.13%

# 3) 이메일 데이터로 학습
def format_email_prompt(example):
    """이메일 다음 단어 예측 형식"""
    return {
        "text": f"[SUBJECT] {example['subject']} [BODY] {example['body']}"
    }

dataset = load_dataset("aeslc", split="train").map(format_email_prompt)

trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    args=SFTConfig(
        output_dir="./email-compose-model",
        num_train_epochs=3,
        per_device_train_batch_size=4,
        learning_rate=2e-4,
        fp16=True,
        logging_steps=50,
    ),
)

trainer.train()
model.save_pretrained("./email-compose-lora")

LoRA vs 전체 파인튜닝 비교 (8B 모델 기준):

전체 파인튜닝 LoRA (r=16) QLoRA (4bit+LoRA)
필요 VRAM ~80GB (A100 2장) ~40GB ~10GB (A100 1장)
학습 시간 중간 길음 (양자화 오버헤드)
성능 최고 거의 동일 약간 낮음
2026 추천 ❌ 비용 과다 ✅ 균형 ✅ GPU 제약 시

2.4.4 샘플링 — 빔 검색 실제 구현

from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

model_path = "./email-compose-lora"  # 파인튜닝된 모델
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.2-3B")
model = AutoModelForCausalLM.from_pretrained(model_path, device_map="auto")

def smart_compose(email_context: str, max_new_tokens: int = 10) -> list[str]:
    """
    스마트 편지쓰기: 이메일 문맥에서 다음 구절 제안

    Args:
        email_context: 현재까지 작성된 이메일 내용
        max_new_tokens: 최대 제안 단어 수
    Returns:
        상위 k개 완성 구절 리스트
    """
    prompt = f"[BODY] {email_context}"
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

    # 빔 검색 (결정적 샘플링) ← 책의 핵심 개념
    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            num_beams=5,            # 빔 너비 k=5
            num_return_sequences=3, # 상위 3개 후보 반환
            early_stopping=True,
            no_repeat_ngram_size=2, # 반복 방지
        )

    # 입력 부분 제거 후 새로 생성된 텍스트만 추출
    input_len = inputs["input_ids"].shape[1]
    suggestions = [
        tokenizer.decode(out[input_len:], skip_special_tokens=True).strip()
        for out in outputs
    ]

    # 후처리: 빈 제안, 너무 긴 제안 필터링
    suggestions = [s for s in suggestions if 1 <= len(s.split()) <= 8]
    return suggestions

# 사용 예시
context = "Hi Ethan! Hope you are"
suggestions = smart_compose(context)
for i, s in enumerate(suggestions, 1):
    print(f"제안 {i}: {context} [TAB] {s}")
# 제안 1: Hi Ethan! Hope you are [TAB] doing well.
# 제안 2: Hi Ethan! Hope you are [TAB] having a great week.
# 제안 3: Hi Ethan! Hope you are [TAB] well.

2.4.5 [심화] 스트리밍 응답 — 100ms 요구사항 달성

from transformers import TextIteratorStreamer
from threading import Thread

def smart_compose_stream(email_context: str):
    """스트리밍으로 토큰을 하나씩 반환해 체감 응답 속도 개선"""
    streamer = TextIteratorStreamer(tokenizer, skip_special_tokens=True)
    inputs = tokenizer(f"[BODY] {email_context}", return_tensors="pt").to(model.device)

    # 백그라운드 스레드에서 생성
    thread = Thread(target=model.generate, kwargs={
        **inputs,
        "max_new_tokens": 10,
        "streamer": streamer,
    })
    thread.start()

    # 토큰이 생성될 때마다 즉시 반환
    for token in streamer:
        yield token  # FastAPI StreamingResponse에 연결

# FastAPI 연동
from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()

@app.get("/compose")
async def compose(context: str):
    return StreamingResponse(
        smart_compose_stream(context),
        media_type="text/plain"
    )

2.5 평가

2.5.1 오프라인 평가 — 라이브러리 활용

import evaluate  # pip install evaluate

# Perplexity 계산
perplexity = evaluate.load("perplexity", module_type="metric")

test_texts = [
    "I hope you are doing well.",
    "Please let me know if you have any questions.",
    "Looking forward to hearing from you.",
]

results = perplexity.compute(
    predictions=test_texts,
    model_id="./email-compose-lora"
)
print(f"평균 Perplexity: {results['mean_perplexity']:.2f}")
# 낮을수록 좋음. 일반 텍스트: ~50, 도메인 특화: ~20 목표
# ExactMatch@N 직접 구현
def exact_match_at_n(predictions: list[str], references: list[str], n: int) -> float:
    """
    생성된 N개 단어가 정답의 첫 N개와 정확히 일치하는 비율

    Args:
        predictions: 모델이 생성한 텍스트 리스트
        references: 실제 정답 텍스트 리스트
        n: 비교할 단어 수
    """
    matches = 0
    for pred, ref in zip(predictions, references):
        pred_words = pred.lower().split()[:n]
        ref_words = ref.lower().split()[:n]
        if pred_words == ref_words:
            matches += 1
    return matches / len(predictions)

# 예시
preds = ["are doing well today", "meeting you today", "are doing well"]
refs  = ["are doing well today", "seeing you today", "are doing well"]

for n in [1, 2, 3, 4]:
    score = exact_match_at_n(preds, refs, n)
    print(f"ExactMatch@{n}: {score:.2f}")
# ExactMatch@1: 0.67
# ExactMatch@2: 0.67
# ExactMatch@3: 0.67
# ExactMatch@4: 0.33

2.5.2 온라인 평가 — 2026년 실무 도구

# LangSmith로 프로덕션 모니터링 (2026년 표준 도구)
# pip install langsmith

from langsmith import Client
from langsmith.wrappers import wrap_openai

client = Client()

# A/B 테스트: 빔 너비 k=3 vs k=5
@client.traceable(run_type="chain", name="smart_compose_v1")
def compose_v1(context: str) -> str:
    return smart_compose(context, num_beams=3)[0]

@client.traceable(run_type="chain", name="smart_compose_v2")
def compose_v2(context: str) -> str:
    return smart_compose(context, num_beams=5)[0]
지표 측정 방법 목표 기준
Perplexity evaluate 라이브러리 < 20 (이메일 도메인)
ExactMatch@3 직접 구현 > 0.4
통과율(수락률) 사용자 로그 분석 > 25% (Gmail 실제 수치)
p50 응답 시간 vLLM 메트릭 < 100ms
p99 응답 시간 vLLM 메트릭 < 300ms

2.6 전체 시스템 설계

2026년 프로덕션 아키텍처

사용자 입력 (React/웹)
    │  WebSocket 스트리밍
    ▼
[API Gateway / Nginx]
    │
    ▼
[트리거링 서비스 — FastAPI]
  단어 수 ≥ 2? + 디바운스 300ms?
    │ Yes → 활성화
    ▼
[구절 생성기 — FastAPI]
  vLLM 서버 호출 (빔 검색)
    │
    ├→ [vLLM 서빙 엔진]
    │    Llama-3.2-3B-LoRA
    │    PagedAttention으로 처리량 최적화
    │
    ├─ 긴 제안 필터 (> 8단어 제거)
    ├─ 신뢰도 필터 (score < 0.1 제거)
    │
    ▼
[후처리 서비스]
  Llama Guard 3 (안전 필터)
  성 중립 대체 규칙
    │
    ▼
사용자에게 제안 스트리밍

트리거링 서비스 구현

from fastapi import FastAPI
import asyncio

app = FastAPI()

class TriggerService:
    """스마트 편지쓰기 활성화 시점 결정"""

    MIN_WORDS = 2          # 최소 단어 수
    DEBOUNCE_MS = 300      # 연속 입력 중 대기 시간

    def should_trigger(self, text: str) -> bool:
        words = text.strip().split()

        # 너무 짧으면 문맥 부족
        if len(words) < self.MIN_WORDS:
            return False

        # 문장 끝(마침표, 줄바꿈)이면 새 문장 시작 대기
        if text.rstrip().endswith(('.', '!', '?', '\n')):
            return False

        return True

trigger = TriggerService()

@app.post("/trigger")
async def check_trigger(text: str):
    return {"should_trigger": trigger.should_trigger(text)}

vLLM 서빙 — 프로덕션 배포

# vLLM 설치 및 서버 시작 (실제 명령어)
pip install vllm

# 파인튜닝된 LoRA 어댑터와 함께 서빙
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3.2-3B \
    --enable-lora \
    --lora-modules email-compose=./email-compose-lora \
    --max-model-len 512 \
    --gpu-memory-utilization 0.9 \
    --port 8000
# vLLM OpenAI 호환 API 호출
from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")

response = client.completions.create(
    model="email-compose",   # LoRA 어댑터 이름
    prompt="[BODY] Hi Ethan, I hope you are",
    max_tokens=10,
    n=3,                      # 빔 검색 결과 3개
    stream=True               # 스트리밍
)

for chunk in response:
    print(chunk.choices[0].text, end="", flush=True)

후처리 서비스 — 편향 제거

import re

class PostProcessor:
    """편향 탐지 및 수정"""

    # 성 편향 대체 규칙
    GENDER_NEUTRAL = {
        r'\bhe or she\b': 'they',
        r'\bhis or her\b': 'their',
        r'\bchairman\b': 'chairperson',
        r'\bpoliceman\b': 'police officer',
        r'\bfireman\b': 'firefighter',
        r'\bstewardess\b': 'flight attendant',
    }

    def process(self, text: str) -> str | None:
        """
        Returns:
            정제된 텍스트, 또는 None (제안 불가 판단 시)
        """
        # 1) 너무 긴 제안 제거
        if len(text.split()) > 8:
            return None

        # 2) 성 중립화
        for pattern, replacement in self.GENDER_NEUTRAL.items():
            text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)

        # 3) Llama Guard로 NSFW 탐지 (실제 구현)
        # if self.safety_model.is_unsafe(text):
        #     return None

        return text

processor = PostProcessor()
print(processor.process("Please ask the chairman for his or her approval"))
# → "Please ask the chairperson for their approval"

연습문제

문제 1 (기초 — 직접 실행 가능)

과제: tiktoken으로 아래 이메일을 토큰화하고, 문자 단위 vs BPE의 어휘집 크기 차이를 출력하라.

text = "Hi Ethan! Haven't seen you for a while. Hope everything's going well."
풀이
import tiktoken

text = "Hi Ethan! Haven't seen you for a while. Hope everything's going well."

# BPE (cl100k_base)
enc = tiktoken.get_encoding("cl100k_base")
bpe_tokens = enc.encode(text)

# 문자 단위 어휘집
char_vocab = set(text)

print(f"BPE 토큰 수: {len(bpe_tokens)}")           # ~16개
print(f"BPE 어휘집 크기: {enc.n_vocab:,}")          # 100,277개
print(f"문자 단위 어휘집 크기: {len(char_vocab)}")   # ~30개

print("\nBPE 토큰:")
for token in bpe_tokens:
    print(f"  {token:6d} → '{enc.decode([token])}'")

문제 2 (중급 — API 기반 빠른 프로토타입)

과제: Claude API로 스마트 편지쓰기 MVP를 구현하라. (경로 A: API 활용)

풀이
import anthropic

client = anthropic.Anthropic()

def smart_compose_api(email_partial: str, num_suggestions: int = 3) -> list[str]:
    """
    Claude API를 이용한 스마트 편지쓰기 MVP
    경로 A (API 활용) — 프로덕션 배포 전 빠른 검증용
    """
    suggestions = []

    for _ in range(num_suggestions):
        response = client.messages.create(
            model="claude-haiku-4-5-20251001",  # 빠르고 저렴한 모델
            max_tokens=20,
            messages=[{
                "role": "user",
                "content": (
                    "Complete this email naturally with 3-8 words. "
                    "Reply with ONLY the completion, nothing else.\n\n"
                    f"Email so far: '{email_partial}'"
                )
            }]
        )
        suggestions.append(response.content[0].text.strip())

    # 중복 제거
    return list(dict.fromkeys(suggestions))

# 테스트
context = "Hi Sarah, I wanted to follow up on our meeting"
for s in smart_compose_api(context):
    print(f"  → {context} | {s}")

문제 3 (고급 — LoRA 파인튜닝 설계)

상황: 3B 파라미터 모델을 이메일 데이터로 LoRA 파인튜닝할 때, r 값을 8, 16, 64로 변경하면 어떤 트레이드오프가 생기는가?

풀이 `r` (랭크)은 LoRA 어댑터의 표현력을 결정한다. | r 값 | 학습 파라미터 (q+v proj, d=3072) | 메모리 | 표현력 | 권장 상황 | |------|-------------------------------|--------|--------|---------| | 8 | 2×2×3072×8 = 98,304 | 최소 | 낮음 | 간단한 스타일 조정 | | 16 | 2×2×3072×16 = 196,608 | 중간 | 중간 | **이메일 완성 추천** | | 64 | 2×2×3072×64 = 786,432 | 높음 | 높음 | 복잡한 태스크 전환 | 실무 기준: 이메일 완성처럼 원래 언어 능력 기반 작업은 `r=16`이 충분하다. `r=64`는 전체 파인튜닝 대비 메모리는 여전히 적지만 과적합 위험이 있다.
# 실험 코드
for r in [8, 16, 64]:
    config = LoraConfig(r=r, lora_alpha=r*2, target_modules=["q_proj", "v_proj"])
    model_copy = get_peft_model(base_model, config)
    trainable, total, pct = model_copy.get_nb_trainable_parameters()
    print(f"r={r:2d}: 학습 파라미터 {trainable/1e6:.2f}M ({pct:.2f}%)")

부록 A: 책 개념 → 2026 도구 매핑

책의 개념 핵심 원리 2026 구현 방법
텍스트 정제 PII 제거, 노이즈 제거 presidio-analyzer
BPE 토큰화 자주 등장하는 쌍 합치기 tiktoken, transformers.AutoTokenizer
텍스트 임베딩 토큰 ID → 벡터 모델 내부 embed_tokens 레이어
위치 인코딩 순서 정보 주입 RoPE (자동 처리, 신경 쓸 일 없음)
사전 학습 일반 언어 학습 HuggingFace에서 다운로드
미세 조정 이메일 특화 학습 LoRA / peft + trl 라이브러리
빔 검색 상위 k 시퀀스 추적 model.generate(num_beams=5)
Perplexity 예측 불확실성 지표 evaluate.load("perplexity")
ExactMatch@N N단어 정확 일치율 직접 구현 (10줄)
트리거링 서비스 언제 제안할지 결정 FastAPI + 디바운스
구절 생성기 후보 생성 + 필터링 vLLM 서빙 엔진
후처리 서비스 편향 제거 규칙 기반 + Llama Guard 3

부록 B: 2026년 스마트 편지쓰기 실전 스택

레이어          선택지                           이유
─────────────────────────────────────────────────────
베이스 모델     Llama 3.2 3B / Gemma 3 4B        작고 빠름, Apache 2.0
파인튜닝        LoRA (r=16) + QLoRA              A100 1장으로 가능
서빙            vLLM                             PagedAttention으로 처리량 최대화
API 서버        FastAPI + asyncio                비동기 스트리밍
안전 필터       Llama Guard 3                    Meta 오픈소스
모니터링        LangSmith / Prometheus           A/B 테스트, 지표 수집
인프라          Modal / Runpod / AWS p4d         GPU 서버리스

부록 C: 설치 명령어 모음

# 핵심 라이브러리 한 번에 설치
pip install \
    transformers \      # 모델 로드/토크나이저
    peft \             # LoRA
    trl \              # SFTTrainer
    bitsandbytes \     # 4비트 양자화
    accelerate \       # 멀티 GPU
    datasets \         # 데이터셋
    evaluate \         # 평가 지표
    tiktoken \         # OpenAI BPE
    vllm \             # 프로덕션 서빙
    fastapi uvicorn \  # API 서버
    presidio-analyzer  # PII 탐지

# vLLM 서버 시작
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3.2-3B \
    --enable-lora \
    --lora-modules email=./email-compose-lora \
    --port 8000
난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플